## Метапрограммирование


Поначалу слово метапрограммирование кажется очень странным и чуждым, но если вы когда-либо работали с декораторами или метаклассами, вы всегда занимались там метапрограммированием. 

В двух словах, мы можем сказать, что метапрограммирование - это код, который манипулирует кодом.

Всю эту мета–штуку можно резюмировать следующим образом: `Метакласс создает классы, а классы создают объекты`


Давайте вначале разберемся, что стоит за этим умным словом. 

Как я уже не раз говорил на наших занятиях, булевы значения, строки, числа, списки, словари и т.п. все это объекты в языке `Python`.
И эти объекты образованы от соответствующих классов (типов данных): 
`bool, str, int, float, list, dict`, функции и т.д. 

Но эти классы также являются и объектами, потому что все в `Python` – это объекты, даже классы.

Да, классы – это объекты, которые позволяют создавать другие объекты с конкретным содержимым. 

А раз классы – это объекты, то должно быть нечто, что создает и их.

И это нечто в `Python` называется метаклассом.

Причем, метакласс – это тоже объект (в `Python` все объекты).

Но это объект особого рода, который нельзя динамически порождать каким-нибудь другим мета-метаклассом.

Он является вершиной, отправной точкой для создания обычных классов и, как следствие, их объектов.

Но что является метаклассом в языке `Python`?

Наверное, вас немного удивит, но это давно знакомый вам объект `type`, который мы использовали для определения типов объектов.

Но это, если он вызывается с одним аргументом. 

Если ему передать три аргумента:

> type(<имя класса>, <кортеж родительских классов>, <словарь с атрибутами и их значениями>)

то данный объект начинает работать совершенно по-другому, а именно, создает (динамически) новый класс, новый тип данных в программе.

В действительности, все стандартные типы данных, функции и обычные классы – порождены метаклассом type. 

В этом легко убедиться, если вызвать функцию type (я ее так называю для удобства) от встроенных типов данных:

```python
type(int)
type(bool)
```
или классов:

```python
class A: pass
type(A)
```
Всюду увидим

```python
<class 'type'>
```
Это, как раз и говорит, что все эти объекты сформированы метаклассом `type`.

Наверное, на этом этапе у вас в голове крутится вопрос: зачем все это надо?

Мы же и так можем объявлять классы. Для чего их создавать динамически в процессе работы программы? 

Чтобы лучше понимать, как функционирует язык `Python`?

И, кроме того, кто знает, возможно в будущем именно вам это и пригодится.

Итак, давайте теперь попробуем создать свой класс с помощью метакласса type. Для простоты сделаем определение класса точки на плоскости:

```python
class Point:
    MAX_COORD = 100
    MIN_COORD = 0
```
Через объект type это будет выглядеть так:

```python
A = type('Point', (), {'MAX_COORD': 100, 'MIN_COORD': 0})
```

Здесь переменная `A` – ссылка на новый созданный класс с именем `Point`. 

Да, так как классы – это тоже объекты, то мы можем на них ссылаться через разные переменные. 

И, далее, можно создать экземпляр этого класса, используя переменную `A`:

```python
pt = A()
```
Вот мы с вами только что динамически в программе сформировали новый класс через метакласс type и увидели, что новый класс корректно работает – создает свои экземпляры.

При необходимости, можно дополнительно прописывать базовые классы, передавая их список вторым аргументом метаклассу type. Например, пусть имеются два класса:

```python
class B1: pass
class B2: pass
```
И, далее, мы можем их указать в качестве базовых при создании нового класса:

```python
A = type('Point', (B1, B2), {'MAX_COORD': 100, 'MIN_COORD': 0})
```
Если теперь вывести коллекцию __mro__ класса A:

```python
A.__mro__
```
то увидим всю цепочку наследования:

```python
(<class '__main__.Point'>, <class '__main__.B1'>, <class '__main__.B2'>, <class 'object'>)
```
Также, при динамическом создании новых классов, в них можно определять и методы. Сделать это можно, по крайней мере, двумя способами. В первом использовать уже существующую функцию, например:

```python
def method1(self):
    print(self.__dict__)
```
и указать ссылку на нее при создании атрибутов класса:

```python
A = type('Point', (), {'method1': method1})
```
Создадим экземпляр класса Point и вызовем этот метод:
```python
pt = A()
pt.method1()
```

Как видите, все работает. 

Во втором способе мы можем определить метод непосредственно в словаре через лямбда-функцию:

```python
A = type('Point', (), {'MAX_COORD': 100, 'method1': lambda self: self.MAX_COORD})
pt = A()
pt.method1()
```

Но так можно определять только простейшие методы. 

Функции со сложным поведением придется задавать отдельно, а затем, добавлять в качестве методов в создаваемый класс.

Я, думаю, вы теперь представляете, что такое метакласс и как можно динамически создавать новые классы в программе с помощью объекта type. 

Однако, описывать сложный алгоритм формирования новых классов непосредственно через объект `type` не очень удобно. 

Мы видели, что даже добавление новых методов происходит несколько непривычно и как-то «коряво». 

Благо, выход есть. В Python мы можем конструировать свои собственные метаклассы, которые, конечно, явно или неявно образуются от объекта `type`.

## Пользовательские метаклассы. Параметр `metaclass`


В `Python` можно описывать свои собственные метаклассы, которые, конечно же, явно или неявно наследуются от основного метакласса `type`.

Давайте предположим, что мы бы хотели создавать классы для точек, в которых бы автоматически появлялись атрибуты `MAX_COORD` и `MIN_COORD`.

Для этого мы создадим метакласс в виде функции (да, обычные функции тоже можно использовать как метаклассы):

```python
def create_class_point(name, base, attrs):
    attrs.update({'MAX_COORD': 100, 'MIN_COORD': 0})
    return type(name, base, attrs)
```

Обратите внимание, функция-метакласс должна иметь три параметра:

 * `name` – имя создаваемого класса; 
 * `base` – кортеж из базовых классов; 
 * `attrs` – словарь с атрибутами класса.

Сейчас в этой функции мы в словарь attrs просто добавляем два атрибута `MAX_COORD` и `MIN_COORD`, а затем, явным вызовом метакласса type формируем новый класс и возвращаем его.

Теперь, чтобы эта функция использовалась в качестве метакласса, при объявлении класса нужно прописать специальный параметр metaclass и передать ссылку на эту функцию:

```python
class Point(metaclass=create_class_point):
    def get_coords(self):
        return (0, 0)
```
Создадим объект этого класса, обратимся к атрибуту `MAX_COORD` и вызовем метод `get_coords`:


```python
pt = Point()
print(pt.MAX_COORD)
print(pt.get_coords())
```
Как видите, все сработало, как мы и ожидали. 

В нашем классе автоматически появились атрибуты `MAX_COORD` и `MIN_COORD`, а также явно, привычным образом, прописали метод `get_coords`, что намного удобнее.

Фактически, здесь класс был создан в функции `create_class_point`, в которую сам язык `Python` передал нужный набор аргументов с соответствующими значениями, а далее, с помощью вызова объекта type был создан этот класс. 

Такой подход намного удобнее, чем прописывать все атрибуты непосредственно в словаре `attrs`.

Однако, использование функции в качестве метакласса – это, скорее, учебный пример для лучшего понимания сущности метаклассов. 

Все же, на практике чаще используют отдельные классы, которые играют роль метаклассов.

Давайте повторим нашу реализацию но не через функцию, а через класс. Вначале объявим класс с именем Meta (имя выбираем сами, это просто пример), который должен наследоваться от метакласса `type`:

```python
class Meta(type):
    def __init__(cls, name, base, attrs):
        super().__init__(name, base, attrs)
        cls.MAX_COORD = 100
        cls.MIN_COORD = 0
```
Внутри этого класса мы прописали инициализатор с четырьмя параметрами. 

Первый `cls` – это ссылка на новый уже созданный класс, а три остальных вам уже знакомы.

 Внутри инициализатора мы вызываем инициализатор базового класса, а затем, динамически добавляем два атрибута `MAX_COORD` и `MIN_COORD`.

А далее все то же самое. Через параметр metaclass указываем метакласс для создания класса `Point` и тестируем его работу:

```python
class Point(metaclass=Meta):
    def get_coords(self):
        return (0, 0)
 
 
pt = Point()
print(pt.MAX_COORD)
print(pt.get_coords())

```

Однако, инициализатор `__init__()` в классе `Meta` вызывается когда класс `Point` полностью создан. 

Для более тонкой работы лучше переопределить магический метод `__new__`, который вызывается непосредственно перед созданием класса. В нашем случае это можно сделать так:

```python
class Meta(type):
    def __new__(cls, name_class, base, attrs):
        attrs.update({'MAX_COORD': 100, 'MIN_COORD': 0})
        return type.__new__(cls, name_class, base, attrs)

```
Здесь записаны четыре параметра: 

* cls – ссылка на текущий класс Meta; 
* name_class – имя создаваемого класса; 
* base – кортеж из базовых классов; 
* attrs – словарь атрибутов создаваемого класса.

Так как метод `__new__` вызывается до создания нового класса, то мы добавляем новые атрибуты `MAX_COORD` и `MIN_COORD` непосредственно в словарь attrs.

А, затем, вызываем аналогичный метод `__new__` у объекта-метакласса `type`. 

Обратите внимание, метод `__new__` должен вернуть ссылку на созданный класс, то есть, обязательно следует прописать оператор `return`.

После запуска программы видим, что класс `Point` создается в соответствии с алгоритмом метакласса `Meta`.

Возможно, здесь у вас возник вопрос, а в чем преимущество использования классов в качестве метаклассов перед функциями?

Принципиальных отличий здесь нет. 

Любой алгоритм можно прописать и на уровне функции и на уровне класса.

Однако, при использовании классов у нас в руках оказывается вся мощь ООП. 
 
В частности, можно создавать иерархии для формирования сложного поведения метаклассов и в крупных проектах это заметно облегчает код и делает его гораздо читабельнее. 

Поэтому на практике чаще все же используют классы из-за их удобства.

Надеюсь, теперь вы представляете как «работают» метаклассы и как можно динамически создавать с их помощью новые классы в программе на `Python`. 

Наверное, здесь остался у вас главный вопрос, где все это можно применить. 

Но об этом я расскажу уже на следующем занятии и покажу все на конкретном примере.